Implement embedded build for cockpit with host app integration - #3
Conversation
A second build target, dist-embed/, serves the same cockpit from a host application (theDAW's SWAY tab) while the desktop app stays untouched. `npm run build:renderer:embed` bundles src/renderer/embed.js, which installs a browser bridge before app.js runs; every window.swaycommand call site works unchanged. The embed differs from dist/ in four ways only: the entry point, a <base href> for the host's mount path, a CSP without the Electron-only frame-src gan:, and templates/docs copied in because there is no main process to read them off disk. The host channel (src/renderer/host/host-channel.js) relays MIDI bytes, audio analysis and tab visibility over postMessage. Windows lets one process hold a MIDI input, so embedded mode never opens the hardware itself; relayed bytes go through the same decode path as the wire. Host analysis frames enter audio.js as the raw read and fall through the same AGC, smoothing and beat detection, decaying back to the local analyser after 500 ms of silence. Three boot fixes found on the way: - requestMIDIAccess() never settles until Chromium's permission prompt is answered. Awaiting it bare left the blast door locked with #boot-status frozen and nothing logged. Bounded to 3 s in midi.js, 6 s per check in runDoctor(), with a late grant still picked up by onstatechange. - `available` reported false in relayed mode, so the splash said WebMIDI was unavailable while relayed MIDI was audibly playing. - The AudioContext built at boot starts suspended under autoplay policy. The ENTER click resumes it; without that the analyser read silence and every scene rendered flat. Plugin postMessage now checks e.source against the gan frame's contentWindow. Embedded, this window has a parent and siblings that can also post to it.
The README now opens on a banner and a still from Miracle Mile, carries a sixteen-scene gallery with a one-line mechanism note under each image, and documents the parts that make the application an instrument rather than a feature list: the cockpit as a line-art diagram plus a region table, the recovered Sway factory map with its CC numbers, the target grammar every assignable destination answers, and the timeline's track chains and sections. Facts were re-checked against the source: sixteen scenes (five folded into the scenes that own their subject last pass), eleven templates, 38 rack parameters in five decks, 14 live track effect kinds, seven synth presets. The wormhole row is gone from the controls table, since the wormhole is an element of Will I Dream now and not a scene. The images are generated, not captured. docs/media/gallery.plan.json is the scene-harness plan behind every still, with the exact io snapshot each scene was photographed under; docs/media/README.md carries the provenance table and the regeneration command. Setup shots that only advance a scene into the state the next shot photographs are prefixed with an underscore. Stills render at 1280x720 and are re-encoded to WebP at 480x270 through a canvas in the same Electron runtime, which keeps the whole asset set at 472 KB. Making that render in the in-app documentation viewer took three small changes. markdown.js grows an image rule, placed ahead of the link rule that would otherwise eat the bracket pair and leave a stray '!'; only the bundle's own media directory resolves, so an external badge falls back to its alt text rather than drawing a broken-image icon under `img-src 'self' data:`. build-renderer.js copies docs/media/ to media/ beside index.html for both targets, images only. Both title derivations (listDocs in main.js, the embed's docs-index) fall back to a leading banner image's alt text, because the README now has no H1: a wordmark image followed by the same word as a heading titles the page twice. LICENSE was missing while package.json, the README and the embed build all declared MIT. It names Daniel Trujillo as the copyright holder and points at the per-file upstream notices for the derived work.
There was a problem hiding this comment.
🟡 Changes recommended
There are confirmed correctness/documented-contract issues in the embedded MIDI permission flow and browser bridge error behavior that can leave features unavailable or crash the renderer.
Once you've addressed the issues Copilot identified, you can request another Copilot review.
Pull request overview
This PR introduces an embedded (non-Electron) build target for the SwayCommand renderer so the “cockpit” can run as a static bundle served by a host app (e.g., embedded in an iframe), with host integration for MIDI/audio/visibility plus documentation/media bundling.
Changes:
- Added a browser/host bridge layer (
window.swaycommandadapter +postMessagechannel) so the renderer can run without Electron preload/IPCs. - Updated MIDI and audio analysis to support host-relayed input when embedded (and improved boot robustness around long-running checks).
- Extended the Markdown/docs pipeline to support local images in the in-app docs viewer, and expanded build/docs assets (README refresh, media plan, dist-embed build output).
File summaries
| File | Description |
|---|---|
| src/renderer/styles.css | Adds styling for Markdown-rendered doc images. |
| src/renderer/midi/midi.js | Adds embedded (host-relayed) MIDI path and a bounded wait for WebMIDI permissions. |
| src/renderer/markdown.js | Adds Markdown image rendering with local-only media rewriting. |
| src/renderer/host/host-channel.js | Implements postMessage channel for host → cockpit MIDI/audio/visibility. |
| src/renderer/host/browser-bridge.js | Implements a browser fallback for the Electron preload window.swaycommand surface. |
| src/renderer/host/bridge.js | Installs the browser bridge + host channel when no native preload exists. |
| src/renderer/engine/audio.js | Consumes host-provided analysis frames when available, falling back to local analyser. |
| src/renderer/embed.js | New embedded entrypoint that installs the host bridge before app boot. |
| src/renderer/app.js | Hardens Doctor checks with timeouts and tightens plugin-frame message source filtering; resumes AudioContext on user gesture. |
| src/main/main.js | Allows docs title extraction from a leading banner image alt text. |
| scripts/build-renderer.js | Adds dist-embed/ build mode, copies docs/templates/media, and stamps build provenance. |
| README.md | Major rewrite with local media gallery and updated development/build instructions. |
| package.json | Adds embed build script(s). |
| LICENSE | Adds MIT license text plus third-party attribution note. |
| docs/media/README.md | Documents provenance/regeneration of README media assets. |
| docs/media/gallery.plan.json | Adds scene-harness plan used to generate README stills. |
| .gitignore | Ignores dist-embed/ build output. |
Review details
Suppressed comments (1)
src/renderer/host/browser-bridge.js:156
readDoc()throws on unknown ids and can throw on fetch failures, which conflicts with the module’s “NOTHING MAY REJECT” guarantee. Even thoughloadDoc()catches today, keeping the bridge non-throwing makes it safer to reuse elsewhere and aligns with the stated design constraint.
- Files reviewed: 16/35 changed files
- Comments generated: 4
- Review effort level: Lite
💡 Add a code-review agent skill or configure MCP servers for context-aware, tailored reviews. Learn more in the docs.
| // requestMIDIAccess does not settle until the user answers Chromium's | ||
| // permission prompt - measured hanging indefinitely when it is never | ||
| // answered. Awaiting it bare wedges main(), leaving the blast door | ||
| // locked with no error anywhere. Give it a bounded wait and carry on | ||
| // without MIDI if it does not arrive; a late grant is picked up by the | ||
| // statechange handler below. | ||
| access = await Promise.race([ | ||
| navigator.requestMIDIAccess({ sysex: false }), | ||
| new Promise((resolve) => setTimeout(() => resolve(null), 3000)), | ||
| ]); | ||
| if (!access) { | ||
| pushMonitor('MIDI permission not answered - continuing without it.'); | ||
| } |
| async function readTemplate(id) { | ||
| const order = await templateIndex(); | ||
| if (!order.includes(id)) throw new Error(`Unknown template: ${id}`); | ||
| const res = await fetchStatic(`templates/${id}.sway`); | ||
| const raw = await res.json(); | ||
| // validateProject lives in src/shared and is bundled; the host module imports | ||
| // it lazily to keep this file free of a hard dependency cycle. | ||
| const { validateProject } = await import('../../shared/swayproject.js'); | ||
| const { doc, warnings } = validateProject(raw); | ||
| return { doc, path: null, dir: null, warnings }; | ||
| } |
| // Every asset load is relative, so one <base> repoints the whole document at | ||
| // the host's mount path. Without it the AudioWorklet and the fonts resolve | ||
| // against the host's root and 404. | ||
| out = out.replace(/<head>/i, `<head>\n <base href="${BASE}" />`); | ||
|
|
| "dist:linux": "npm run build:icon && npm run build:renderer && electron-builder --linux", | ||
| "build:renderer:embed": "node scripts/build-renderer.js --embed --base=/sway-app/", | ||
| "build:all": "npm run build:renderer && npm run build:renderer:embed" |
This pull request adds a new
LICENSEfile to the repository. The license used is the MIT License, which allows for broad use, modification, and distribution of the software with minimal restrictions. The file also includes a note about third-party code and attribution.Licensing:
LICENSEfile with the MIT License, granting permission for use, modification, and distribution, and including a notice about third-party code and attributions.